<?php
/**
 * JSON
 * 
 * @category	Util
 * @package		dayscript.util
 * @subpackage	
 * @author		Nelson Daza <ndaza@dayscript.com>
 * @copyright	2009 Dayscript Ltda.
 * @license		
 * @version		1.0
 * @version		$Revision: 0 $
 * @filesource	
 * @link		http://www.dayscript.com
 * @link		{docsLink}
 * @uses		
 * @since		1.0
 * @modifiedby	$LastChangedBy: Nelson Daza $
 * @modified	$Date: 2009-08-25 $
 */

namespace dayscript\util;

/**
 * JSON
 *
 * Singleton
 * Converts to and from JSON format.
 * 
 * @category	Util
 * @package		dayscript.util
 * @subpackage	
 * @author		Nelson Daza <ndaza@dayscript.com>
 * @copyright	2009 Dayscript Ltda.
 * @license		
 * @version		1.0
 * @link		http://www.dayscript.com
 * @link		{docsLink}
 * @uses		\Countable, \Iterator, \Serializable, \ArrayAccess
 * @since		1.0
 * @modifiedby	$LastChangedBy: Nelson Daza $
 * @modified	$Date: 2009-08-13 $
 */
class JSON	{
	/**#@+
	 * Marker constant for decode( ), used to flag stack state
	 * @var int
	 */
	const STATE_SLICE = 1;
	const STATE_IN_STRING = 2;
	const STATE_IN_ARRAY = 3;
	const STATE_IN_OBJECT = 4;
	const STATE_IN_COMMENT = 5;
	/**
	 * JSON constructor
	 * @return \dayscript\util\JSON
	 */
	private function __construct( )	{

	}
	/**
	 * Encodes an arbitrary variable into JSON format
	 * @param mixed $var Any thing to be encoded. If var is a strng, note that encode( ) always expects it to be in ASCII or UTF-8 format!
	 * @return mixed JSON string representation.
	 */
	public static function encode( $var )	{
		switch ( \gettype( $var ) )	{
			case 'boolean':
				return $var ? 'true' : 'false';
			case 'NULL':
				return 'null';
			case 'integer':
				return (int) $var;
			case 'double':
			case 'float':
				return (float) $var;
			case 'string':
				// STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT
				return '"' . self::scapeString( $var ) . '"';
			case 'array':
				$properties = array( );
				if ( !\dayscript\util\Util::isVector( $var ) )	{
					foreach( $var as $key => $value )
						$properties[] = self::encode( \strval( $key ) ) . ':' . self::encode( $value );
					return '{' . \implode( ',', $properties ) . '}';
				}
				
				foreach( $var as $value )
					$properties[] = self::encode( $value );
				
				return '[' . \implode( ',', $properties ) . ']';
			case 'object':
				$vars = \get_object_vars( $var );
				$properties = array( );
				foreach( $vars as $key => $value )
					$properties[] = self::encode( \strval( $key ) ) . ':' . self::encode( $value );
				if( $var instanceOf \Iterator )	{
					foreach( $var as $key => $value )
						$properties[] = self::encode( \strval( $key ) ) . ':' . self::encode( $value );
				}
					
				return '{' . \implode( ',', $properties ) . '}';
			default:
				;
		}
		throw new \Exception( 'Parameter "' . \gettype( $var ) . '" Can\'t be enconded as JSON string' );
	}
	/**
	 * Iterates over every character in the string, escaping with a slash or encoding to UTF-8 where necessary
	 * @param string $string String to scape
	 * @return string $escaped Scaped string
	 */
	protected static function scapeString( $string )	{
		$length = \strlen( $string );
		$escaped = '';
		for ( $c = 0; $c < $length; $c++ )	{
			$char = \ord( $string{$c} );
			if( $char == 0x08 )
				$escaped .= '\b';
			else if( $char == 0x09 )
				$escaped .= '\t';
			else if( $char == 0x0A )
				$escaped .= '\n';
			else if( $char == 0x0C )
				$escaped .= '\f';
			else if( $char == 0x0D )
				$escaped .= '\r';
			else if( $char == 0x22 || $char == 0x2F || $char == 0x5C )	// double quote, slash, slosh
				$escaped .= '\\' . $string{$c};
			else if( $char >= 0x20 && $char <= 0x7F )	// characters U-00000000 - U-0000007F (same as ASCII)
				$escaped .= $string{$c};
			else	{
				if( ( $char & 0xE0 ) == 0xC0 )	{	  // characters U-00000080 - U-000007FF, mask 110XXXXX
					$char = \pack( 'C*', $char, \ord( $string{$c + 1} ) );
					$c += 1;
				}
				else if( ( $char & 0xF0 ) == 0xE0 )	{ // characters U-00000800 - U-0000FFFF, mask 1110XXXX
					$char = pack( 'C*', $char, \ord( $string{$c + 1} ), \ord( $string{$c + 2} ) );
					$c += 2;
				}
				else if( ( $char & 0xF8 ) == 0xF0 )	{ // characters U-00010000 - U-001FFFFF, mask 11110XXX
					$char = pack( 'C*', $char, \ord( $string{$c + 1} ), \ord( $string{$c + 2} ), \ord( $string{$c + 3} ) );
					$c += 3;
				}
				else if( ( $char & 0xFC ) == 0xF8 )	{ // characters U-00010000 - U-001FFFFF, mask 11110XXX
					$char = pack( 'C*', $char, \ord( $string{$c + 1} ), \ord( $string{$c + 2} ), \ord( $string{$c + 3}, \ord( $string{$c + 4} ) ) );
					$c += 4;
				}
				else if( ( $char & 0xFE ) == 0xFC )	{ // characters U-00010000 - U-001FFFFF, mask 11110XXX
					$char = pack( 'C*', $char, \ord( $string{$c + 1} ), \ord( $string{$c + 2} ), \ord( $string{$c + 3}, \ord( $string{$c + 4} ), \ord( $string{$c + 5} ) ) );
					$c += 5;
				}
				$utf16 = \dayscript\Util::charUTF8ToUTF16( $char );
				$escaped .= \sprintf( '\u%04s', \bin2hex( $utf16 ) );
			}
		}
		return $escaped;
	}
	/**
	 * Decodes a JSON string into appropriate variable.
	 * @param string $str JSON-formatted string.
	 * @param boolean $createObjects Whether return strClass objects or arrays instead. 
	 * @return mixed $decoded Number, boolean, string, array, or object corresponding to given JSON input string. Note that decode() always returns strings in ASCII or UTF-8 format.
	 */
	public static function decode( $str, $createObjects = false )	{
		$str = self::reduceString( $str );

		switch ( \strtolower( $str ) )	{
			case 'true':
				return true;
			case 'false':
				return false;
			case 'null':
				return null;
			default:
				$m = array( );
				// Number
				if ( \is_numeric( $str ) )	{
					// Yu huuuu!!
					return ( (float)$str == (integer)$str ) ? (integer)$str : (float)$str;
				}
				// String
				else if ( \preg_match( '/^("|\').*(\1)$/s', $str, $m ) && $m[1] == $m[2] )	{
					$delim = \substr( $str, 0, 1 );
					$chrs = \substr( $str, 1, -1 );
					$utf8 = '';
					$strlen_chrs = \strlen( $chrs );

					for( $c = 0; $c < $strlen_chrs; $c++ )	{
						$substr_chrs_c_2 = \substr( $chrs, $c, 2 );
						$ord_chrs_c = \ord( $chrs{$c} );
						switch ( true )	{
							case $substr_chrs_c_2 == '\b':
								$utf8 .= \chr(0x08);
								++$c;
								break;
							case $substr_chrs_c_2 == '\t':
								$utf8 .= \chr(0x09);
								++$c;
								break;
							case $substr_chrs_c_2 == '\n':
								$utf8 .= \chr(0x0A);
								++$c;
								break;
							case $substr_chrs_c_2 == '\f':
								$utf8 .= \chr(0x0C);
								++$c;
								break;
							case $substr_chrs_c_2 == '\r':
								$utf8 .= \chr(0x0D);
								++$c;
								break;
							case $substr_chrs_c_2 == '\\"':
							case $substr_chrs_c_2 == '\\\'':
							case $substr_chrs_c_2 == '\\\\':
							case $substr_chrs_c_2 == '\\/':
								if ( ( $delim == '"' && $substr_chrs_c_2 != '\\\'' ) || ( $delim == "'" && $substr_chrs_c_2 != '\\"' ) )
									$utf8 .= $chrs{++$c};
								break;
							case \preg_match( '/\\\u[0-9A-F]{4}/i', \substr( $chrs, $c, 6 ) ):
								// single, escaped unicode character
								$utf16 = \chr( \hexdec( \substr( $chrs, ( $c + 2 ), 2 ) ) ) . \chr( \hexdec( \substr( $chrs, ( $c + 4 ), 2 ) ) );
								$utf8 .= \dayscript\util\Util::charUTF16ToUTF8( $utf16 );
								$c += 5;
								break;
							case ( $ord_chrs_c >= 0x20 ) && ( $ord_chrs_c <= 0x7F ):
								$utf8 .= $chrs{$c};
								break;
							case ( $ord_chrs_c & 0xE0 ) == 0xC0:
								// characters U-00000080 - U-000007FF, mask 110XXXXX
								$utf8 .= \substr( $chrs, $c, 2 );
								++$c;
								break;
							case ( $ord_chrs_c & 0xF0 ) == 0xE0:
								// characters U-00000800 - U-0000FFFF, mask 1110XXXX
								$utf8 .= \substr( $chrs, $c, 3 );
								$c += 2;
								break;
							case ( $ord_chrs_c & 0xF8 ) == 0xF0:
								// characters U-00010000 - U-001FFFFF, mask 11110XXX
								$utf8 .= \substr( $chrs, $c, 4 );
								$c += 3;
								break;
							case ( $ord_chrs_c & 0xFC ) == 0xF8:
								// characters U-00200000 - U-03FFFFFF, mask 111110XX
								$utf8 .= substr($chrs, $c, 5);
								$c += 4;
								break;

							case ($ord_chrs_c & 0xFE) == 0xFC:
								// characters U-04000000 - U-7FFFFFFF, mask 1111110X
								// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
								$utf8 .= \substr( $chrs, $c, 6 );
								$c += 5;
								break;
						}
					}
					return $utf8;
				}
				// Array or Object
				else if ( \preg_match( '/^\[.*\]$/s', $str ) || \preg_match( '/^\{.*\}$/s', $str ) )	{
					$arr = array( );
					$obj = ( $createObjects ? new \stdClass( ) : array( ) );
					$stk = ( $str{0} == '[' ? array( self::STATE_IN_ARRAY ) : array( self::STATE_IN_OBJECT ) );
					
					\array_push( $stk, array( 'what'  => self::STATE_SLICE, 'where' => 0, 'delim' => false ) );

					$chrs = \substr( $str, 1, -1 );
					$chrs = self::reduceString( $chrs );

					if ( $chrs == '' )	{
						if ( \reset( $stk ) == self::STATE_IN_ARRAY )
							return $arr;
						else
							return $obj;
					}

					$strlen_chrs = \strlen( $chrs );
					for ( $c = 0; $c <= $strlen_chrs; $c++ )	{
						$top = \end( $stk );
						$substr_chrs_c_2 = \substr( $chrs, $c, 2 );
						// Found a comma that is not inside a string, array, etc., OR we've reached the end of the character list
						if ( ( $c == $strlen_chrs ) || ( ( $chrs{$c} == ',' ) && ( $top['what'] == self::STATE_SLICE ) ) )	{
							$slice = \substr( $chrs, $top['where'], ( $c - $top['where'] ) );
							\array_push( $stk, array( 'what' => self::STATE_SLICE, 'where' => ($c + 1), 'delim' => false ) );
							if ( \reset( $stk ) == self::STATE_IN_ARRAY )	{
								// We are in an array, so just push an element onto the stack
								\array_push( $arr, self::decode( $slice, $createObjects ) );
							}
							else if ( \reset( $stk ) == self::STATE_IN_OBJECT )	{
								// we are in an object, so figure out the property name and set an element in an associative array, for now
								$parts = array( );
								if ( \preg_match( '/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts ) )	{
									// "name":value pair
									$key = self::decode( $parts[1], $createObjects );
									$val = self::decode( $parts[2], $createObjects );

									if ( !$createObjects )
										$obj[$key] = $val;
									else
										$obj->$key = $val;
								}
								else if ( \preg_match( '/^\s*(\w+)\s*:\s*(\S.*),?$/Uis', $slice, $parts ) )	{
									// name:value pair, where name is unquoted
									$key = $parts[1];
									$val = self::decode( $parts[2], $createObjects );

									if ( !$createObjects )
										$obj[$key] = $val;
									else
										$obj->$key = $val;
								}
							}
						}
						else if ( ( ( $chrs{$c} == '"' ) || ( $chrs{$c} == "'" ) ) && ( $top['what'] != self::STATE_IN_STRING ) )	{
							// found a quote, and we are not inside a string
							\array_push( $stk, array( 'what' => self::STATE_IN_STRING, 'where' => $c, 'delim' => $chrs{$c} ) );
						}
						else if ( ( $chrs{$c} == $top['delim'] ) && ( $top['what'] == self::STATE_IN_STRING ) && ( ( \strlen( \substr( $chrs, 0, $c ) ) - \strlen( \rtrim( \substr( $chrs, 0, $c ), '\\' ) ) ) % 2 != 1 ) )	{
							// Found a quote, we're in a string, and it's not escaped we know that it's not escaped becase there is _not_ an odd number of backslashes at the end of the string so far
							\array_pop( $stk );
						}
						else if ( ( $chrs{$c} == '[' ) && \in_array( $top['what'], array( self::STATE_SLICE, self::STATE_IN_ARRAY, self::STATE_IN_OBJECT ) ) )	{
							// Found a left-bracket, and we are in an array, object, or slice
							\array_push( $stk, array( 'what' => self::STATE_IN_ARRAY, 'where' => $c, 'delim' => false ) );
						}
						else if ( ( $chrs{$c} == ']' ) && ( $top['what'] == self::STATE_IN_ARRAY ) )	{
							// found a right-bracket, and we're in an array
							\array_pop( $stk );
						}
						else if ( ( $chrs{$c} == '{' ) && \in_array( $top['what'], array( self::STATE_SLICE, self::STATE_IN_ARRAY, self::STATE_IN_OBJECT ) ) )	{
							// found a left-brace, and we are in an array, object, or slice
							\array_push( $stk, array( 'what' => self::STATE_IN_OBJECT, 'where' => $c, 'delim' => false ) );
						}
						else if ( ( $chrs{$c} == '}' ) && ( $top['what'] == self::STATE_IN_OBJECT ) )	{
							// found a right-brace, and we're in an object
							\array_pop( $stk );
						}
						else if ( ( $substr_chrs_c_2 == '/*' ) && \in_array( $top['what'], array( self::STATE_SLICE, self::STATE_IN_ARRAY, self::STATE_IN_OBJECT ) ) )	{
							// found a comment start, and we are in an array, object, or slice
							\array_push( $stk, array( 'what' => self::STATE_IN_COMMENT, 'where' => $c, 'delim' => false ) );
							$c++;
						}
						else if ( ( $substr_chrs_c_2 == '*/' ) && ( $top['what'] == self::STATE_IN_COMMENT ) )	{
							// found a comment end, and we're in one now
							\array_pop( $stk );
							$c++;
							for ( $i = $top['where']; $i <= $c; ++$i )
								$chrs = \substr_replace( $chrs, ' ', $i, 1 );
						}
					}

					if ( \reset($stk) == self::STATE_IN_ARRAY )
						return $arr;
					else if ( \reset($stk) == self::STATE_IN_OBJECT )
						return $obj;
				}
		} // End Switch 
	}
	/**
	 * Reduce a string by removing leading and trailing comments and whitespace
	 * @param string $str String value to strip of comments and whitespace
	 * @return string String value stripped of comments and whitespace
	 */
	protected static function reduceString( $str )	{
		$str = \preg_replace( array(
		// eliminate single line comments in '// ...' form
			'#^\s*//(.+)$#m',
		// eliminate multi-line comments in '/* ... */' form, at start of string
			'#^\s*/\*(.+)\*/#Us',
		// eliminate multi-line comments in '/* ... */' form, at end of string
			'#/\*(.+)\*/\s*$#Us' ), '', $str );
		// eliminate extraneous space
		return \trim( $str );
	}
}
